#!/usr/bin/python
#
# Copyright (c) 2008, Mathias Weber
# Copyright (c) 2008, Thomas Stauffer
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# $Id: example-pg.py 109 2009-06-23 22:30:50Z Azraiyl $

"""
Everything inside this file is procedurally generated.

References for the "Superformula"

http://local.wasp.uwa.edu.au/~pbourke/surfaces_curves/supershape
http://local.wasp.uwa.edu.au/~pbourke/surfaces_curves/supershape3d
http://en.wikipedia.org/wiki/Superellipse
http://en.wikipedia.org/wiki/Superformula

Here you can get some basic information about Perlin Noise, but it is IMO not
accurate, but good for an introduction.

http://freespace.virgin.net/hugo.elias/models/m_perlin.htm
"""

import sys
import math
import time
import random

from pandac.PandaModules import Geom
from pandac.PandaModules import GeomNode
from pandac.PandaModules import GeomPoints
from pandac.PandaModules import GeomTriangles
from pandac.PandaModules import GeomVertexData
from pandac.PandaModules import GeomVertexFormat
from pandac.PandaModules import GeomVertexWriter
from pandac.PandaModules import NodePath
from pandac.PandaModules import PNMImage
from pandac.PandaModules import Texture
from pandac.PandaModules import VBase3, VBase2

import direct.directbase.DirectStart

base.setBackgroundColor(0.0, 0.0, 0.0)
base.disableMouse()

camera.setPos(0.0, -10.0, 5.0)
camera.lookAt(0.0, 0.0, 0.0)

def rangeex(start, end, step):
    current = start
    while current <= end:
        yield current
        current += step

def clamp(value, min, max):
    if value < min:
        return min
    if value > max:
        return max
    return value

class NoiseTextureMaker:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def generate(self):
        """
        You can also generate noise with the Panda3D class PerlinNoise. e.g.

        noise = PerlinNoise2()
        color = noise.noise(x, y)

        IMO it should be faster than random.random but it is not.
        """

        image = PNMImage(self.width, self.height, 3)

        t1 = time.time()

        for y in range(self.height):
            for x in range(self.width):
                color = random.random()
                image.setXel(x, y, 0.0, 0.0, color)

        t2 = time.time()
        print "NoiseTextureMaker.generate", t2 - t1, "s"

        texture = Texture("NoiseTextureMaker")
        texture.load(image)
        return texture

class VoronoiTextureMaker():
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def generate(self):
        image = PNMImage(self.width, self.height, 3)

        t1 = time.time()

        points = []
        for i in range(5):
            x = random.randint(0, self.width)
            y = random.randint(0, self.height)
            points += [ [x, y] ]

        for y in range(self.height):
            for x in range(self.width):
                nearest = []
                for p in points:
                    dx = x - p[0]
                    dy = y - p[1]
                    d = dx * dx + dy * dy
                    nearest += [ d ]
                nearest.sort()
                f = float(nearest[0]) / float(nearest[1])
                image.setXel(x, y, 0.0, f, 0.0)

        """
        for p in points:
            x, y = p[0], p[1]
            for i in [-1, 0, 1]:
                for j in [-1, 0, 1]:
                    image.setXel(x + i, y + j, 1.0, 1.0, 1.0)
        """

        t2 = time.time()
        print "VoronoiTextureMaker.generate", t2 - t1, "s"

        texture = Texture("NoiseTextureMaker")
        texture.load(image)
        return texture


class CubeMeshMaker:
    def __init__(self, size):
        self.size = size

    def generate(self):
        format = GeomVertexFormat.getV3t2()
        data = GeomVertexData("Data", format, Geom.UHStatic)
        vertices = GeomVertexWriter(data, "vertex")
        texcoords = GeomVertexWriter(data, "texcoord")
        triangles = GeomTriangles(Geom.UHStatic)

        def add(v0, v1, v2, v3):
            i = vertices.getWriteRow()
            vertices.addData3f(v0)
            vertices.addData3f(v1)
            vertices.addData3f(v2)
            vertices.addData3f(v3)
            texcoords.addData2f(0.0, 0.0)
            texcoords.addData2f(1.0, 0.0)
            texcoords.addData2f(1.0, 1.0)
            texcoords.addData2f(0.0, 1.0)
            triangles.addVertices(i + 0, i + 1, i + 2)
            triangles.addVertices(i + 0, i + 2, i + 3)
            triangles.closePrimitive()

        v0 = VBase3(-self.size, -self.size, -self.size)
        v1 = VBase3(+self.size, -self.size, -self.size)
        v2 = VBase3(-self.size, +self.size, -self.size)
        v3 = VBase3(+self.size, +self.size, -self.size)
        v4 = VBase3(-self.size, -self.size, +self.size)
        v5 = VBase3(+self.size, -self.size, +self.size)
        v6 = VBase3(-self.size, +self.size, +self.size)
        v7 = VBase3(+self.size, +self.size, +self.size)

        add(v4, v5, v7, v6) # Z+
        add(v2, v3, v1, v0) # Z-
        add(v1, v3, v7, v5) # X+
        add(v2, v0, v4, v6) # X-
        add(v3, v2, v6, v7) # Y+
        add(v0, v1, v5, v4) # Y-

        geom = Geom(data)
        geom.addPrimitive(triangles)

        node = GeomNode("CubeMeshMaker")
        node.addGeom(geom)

        return NodePath(node)

class SpherePointsoupMaker:
    def __init__(self):
        self.step = 0.1

    def generate(self):
        format = GeomVertexFormat.getV3()
        data = GeomVertexData("Data", format, Geom.UHStatic)
        vertices = GeomVertexWriter(data, "vertex")
        points = GeomPoints(Geom.UHStatic)

        t1 = time.time()

        r = 1.0
        for a in rangeex(0, 2 * math.pi, self.step):
            for b in rangeex(-math.pi / 2.0, +math.pi / 2.0, self.step):
                x = r * math.cos(a) * math.cos(b)
                y = r * math.sin(a) * math.cos(b)
                z = r * math.sin(b)

                i = vertices.getWriteRow()
                vertices.addData3f(x, y, z)
                points.addVertex(i)
        points.closePrimitive()

        t2 = time.time()
        print "SpherePointsoupMaker.generate", t2 - t1, "s"

        geom = Geom(data)
        geom.addPrimitive(points)

        node = GeomNode("CubeMeshMaker")
        node.addGeom(geom)

        return NodePath(node)

class SuperShapeMeshMaker:
    def __init__(self):
        self.step = 0.05

    def generate(self):
        format = GeomVertexFormat.getV3c4()
        data = GeomVertexData("Data", format, Geom.UHStatic)
        vertices = GeomVertexWriter(data, "vertex")
        colors = GeomVertexWriter(data, "color")
        triangles = GeomTriangles(Geom.UHStatic)

        t1 = time.time()

        def sf(x, a, b, m, n1, n2, n3):
            s = abs(math.cos(m * x / 4.0) / a) ** n2
            t = abs(math.sin(m * x / 4.0) / b) ** n3
            u = (s + t) ** (-1.0 / n1)
            return u

        sf1 = lambda x: sf(x, 1.0, 1.0, 4.0, 4.0, 7.0, 7.0)
        sf2 = lambda x: sf(x, 1.0, 1.0, 4.0, 4.0, 7.0, 7.0)

        #sf1 = lambda x: sf(x, 1.0, 1.0, 7.0, 0.2, 1.7, 1.7)
        #sf2 = lambda x: sf(x, 1.0, 1.0, 7.0, 0.2, 1.7, 1.7)

        #sf1 = lambda x: sf(x, 1.0, 1.0, 5.0, 0.1, 1.7, 1.7)
        #sf2 = lambda x: sf(x, 1.0, 1.0, 1.0, 0.3, 0.5, 0.5)

        def col(color):
            return clamp(abs(color * 2.0), 0.0, 1.0)

        """
        This procedure is highly inefficient and not correct at all. First every
        point is calculated for 4 times to often, second the last segments may
        not overlap perfectly.
        """

        for rho in rangeex(-math.pi, +math.pi, self.step):
            rho1 = rho
            rho2 = rho + self.step

            sf1rho1 = sf1(rho1)
            sf1rho2 = sf1(rho2)

            cosrho1 = math.cos(rho1)
            sinrho1 = math.sin(rho1)
            cosrho2 = math.cos(rho2)
            sinrho2 = math.sin(rho2)

            for phi in rangeex(-math.pi / 2.0, +math.pi / 2.0, self.step):
                phi1 = phi
                phi2 = phi + self.step

                sf2phi1 = sf2(phi1)
                sf2phi2 = sf2(phi2)

                cosphi1 = math.cos(phi1)
                sinphi1 = math.sin(phi1)
                cosphi2 = math.cos(phi2)
                sinphi2 = math.sin(phi2)

                x11 = 1.0 * cosrho1 * 1.0 * cosphi1
                y11 = 1.0 * sinrho1 * 1.0 * cosphi1
                z11 = 1.0 * sinphi1

                x12 = 1.0 * cosrho2 * 1.0 * cosphi1
                y12 = 1.0 * sinrho2 * 1.0 * cosphi1
                z12 = 1.0 * sinphi1

                x21 = 1.0 * cosrho2 * 1.0 * cosphi2
                y21 = 1.0 * sinrho2 * 1.0 * cosphi2
                z21 = 1.0 * sinphi2

                x22 = 1.0 * cosrho1 * 1.0 * cosphi2
                y22 = 1.0 * sinrho1 * 1.0 * cosphi2
                z22 = 1.0 * sinphi2

                d0 = rho - math.atan2(y11, x11)
                if d0 > 1e-6:
                    print "Oo1", d0
                d1 = phi - math.asin(z11)
                #print phi, math.asin(z11), d1
                #d1 = phi - math.atan2(z11, abs(x11))
                if d1 > 1e-6:
                    print "Oo2", d1


        """
        for rho in rangeex(-math.pi, +math.pi, self.step):
            rho1 = rho
            rho2 = rho + self.step

            sf1rho1 = sf1(rho1)
            sf1rho2 = sf1(rho2)

            cosrho1 = math.cos(rho1)
            sinrho1 = math.sin(rho1)
            cosrho2 = math.cos(rho2)
            sinrho2 = math.sin(rho2)

            for phi in rangeex(-math.pi / 2.0, +math.pi / 2.0, self.step):
                phi1 = phi
                phi2 = phi + self.step

                sf2phi1 = sf2(phi1)
                sf2phi2 = sf2(phi2)

                cosphi1 = math.cos(phi1)
                sinphi1 = math.sin(phi1)
                cosphi2 = math.cos(phi2)
                sinphi2 = math.sin(phi2)

                x11 = sf1rho1 * cosrho1 * sf2phi1 * cosphi1
                y11 = sf1rho1 * sinrho1 * sf2phi1 * cosphi1
                z11 = sf2phi1 * sinphi1

                x12 = sf1rho2 * cosrho2 * sf2phi1 * cosphi1
                y12 = sf1rho2 * sinrho2 * sf2phi1 * cosphi1
                z12 = sf2phi1 * sinphi1

                x21 = sf1rho2 * cosrho2 * sf2phi2 * cosphi2
                y21 = sf1rho2 * sinrho2 * sf2phi2 * cosphi2
                z21 = sf2phi2 * sinphi2

                x22 = sf1rho1 * cosrho1 * sf2phi2 * cosphi2
                y22 = sf1rho1 * sinrho1 * sf2phi2 * cosphi2
                z22 = sf2phi2 * sinphi2

                i = vertices.getWriteRow()

                vertices.addData3f(x11, y11, z11)
                vertices.addData3f(x12, y12, z12)
                vertices.addData3f(x21, y21, z21)
                vertices.addData3f(x22, y22, z22)

                colors.addData4f(col(x11), col(y11), col(z11), 1.0)
                colors.addData4f(col(x12), col(y12), col(z12), 1.0)
                colors.addData4f(col(x21), col(y21), col(z21), 1.0)
                colors.addData4f(col(x22), col(y22), col(z22), 1.0)

                triangles.addVertex(i + 0)
                triangles.addVertex(i + 1)
                triangles.addVertex(i + 2)
                triangles.closePrimitive()
                triangles.addVertex(i + 0)
                triangles.addVertex(i + 2)
                triangles.addVertex(i + 3)
                triangles.closePrimitive()
        """

        t2 = time.time()
        print "SuperShapeMeshMaker.generate", t2 - t1, "s"

        geom = Geom(data)
        geom.addPrimitive(triangles)

        node = GeomNode("SuperShapeMeshMaker")
        node.addGeom(geom)

        return NodePath(node)

texture1 = NoiseTextureMaker(128, 128).generate()
mesh1 = CubeMeshMaker(1.0).generate()
mesh1.setTexture(texture1)
mesh1.reparentTo(render)
mesh1.setPos(-2.0, -2.0, 0.0)

texture2 = VoronoiTextureMaker(128, 128).generate()
mesh2 = CubeMeshMaker(1.0).generate()
mesh2.setTexture(texture2)
mesh2.reparentTo(render)
mesh2.setPos(2.0, -2.0, 0.0)

mesh3 = SpherePointsoupMaker().generate()
mesh3.reparentTo(render)
mesh3.setPos(-2.0, 2.0, 0.0)

mesh4 = SuperShapeMeshMaker().generate()
mesh4.reparentTo(render)
mesh4.setPos(2.0, 2.0, 0.0)

base.accept("escape", sys.exit)
base.accept("a", render.analyze)
base.accept("o", base.oobe)

run()
